上一篇介紹了動態型別和靜態型別的差別,這一篇來看到弱型別和強型別。
強型別的例子,我們一樣拿型別界的乖寶寶 —— Java 為例,企圖在數字運算過程混進一個字串:
int x = 123 + "456";
System.out.println(x);
不出所料,得到一個編譯錯誤:
HelloWorld.java:4: error: incompatible types: String cannot be converted to int
很明確的告訴你,在 Java 不能用隱喻的方式,企圖直接將字串轉成整數,如果你想實現的是將「數字 123
」和「字串 "456"
轉成的數字」相加,你必須用 Java 能接受的方式明確表明,比如:
int x = 123 + Integer.parseInt("456");
System.out.println(x);
執行結果:
579
這就是強型別的語言,偏向不容忍隱性的型別轉換。
同樣的寫法,換成弱型別會發生什麼事?
以 PHP 為例:
$x = 123 + "456";
echo $x;
執行結果:
579
不僅成功執行,沒有錯誤訊息,還成功印出字串 "456"
轉成數字後的相加結果。
這就是弱型別的語言,這類語言偏向容忍隱性的型別轉換。雖然沒有在程式碼裡指明我想將字串 "456"
轉成數字,PHP 直譯器發現我在做算術運算,而且 "456"
可以被轉成數字,就「貼心」地自動幫忙轉成數字,然後吐出相加的結果。
可能會說:這樣不是很好嗎?不用什麼事都要在語法上囉哩吧嗦指定清楚,程式自動就能「體察上意」。
在許多時候確實很方便省事,但有時真的不小心打錯程式,電腦不會知道你是故意還是不小心,一樣會自作聰明去「猜」你想做什麼,自動幫你進行你不想要的轉型。
例如下面這個例子:
$n1 = 123;
$n2 = 456;
$s1 = "Hello";
$x = $n1 + $s1;
echo $x;
執行結果:
123
PHP Warning: A non-numeric value encountered in /home/cg/root/6938116/main.php on line 4
我想做的其實是 $x = $n1 + $n2
,手誤為 $x = $n1 + $s1
,PHP 直譯器發現我在做算術運算,於是擅自將 "Hello"
硬轉型成數字,得到 0
,繼續產生執行結果 123
。而不是盡早提示錯誤、終止程式,必須等開發者自己發覺。
確實,"Hello"
根本不適合轉成數字,PHP 直譯器覺得怪怪的,幫忙顯示一個 PHP Warning 來提醒開發者。但 Warning 不等於 Error,對電腦來說,這段程式碼仍是成功運作,會繼續往下面的程式碼執行。但實際上我們得到了非預期的結果,後續執行的程式碼連帶可能也都是錯誤的結果。
PHP 環境設定需要開啟 Warning 才會印出警告,開發環境通常會開啟以便 debug,生產環境通常會關閉。
弱型別最常導致 Bug 的情境,除了混用不同型別去數學運算或字串串接,另一個就是允許不同型別之間的比對。
以 PHP 為例,要比對兩個值,提供了兩種運算子:
==
:寬鬆比對,只比對兩值的內容。===
:嚴謹比對,比對兩值的型別和內容。echo ("111" == 111) ? "Yes" : "No"; // "Yes"
echo ("111" === 111) ? "Yes" : "No"; // "No"
簡單來說,就是編譯器或直譯器對型別檢查的寬容程度。
或更淺白地形容:允許編譯器或直譯器自作主張的程度。
強型別語言偏向說一是一、說二是二,你沒有在程式語法上明確指示就是沒這件事,發現不是正常寫法,直接停下來告訴你發生錯誤。
弱型別語言就不同了,發現不是正常寫法,會試圖去做一些自動轉型,讓這段程式繼續運作下去。
至於孰優孰劣?其實沒有絕對標準。
普遍來說,強型別當然比較嚴謹,很多潛藏的 Bug 可以在編譯時期甚至撰寫時期就發現;但也代表開發過程可能需要花許多時間去雕琢語法來符合強型別的語法規範限制。
而弱型別風險相對較高,但撰寫時不會有那麼多限制。
值得一提的是,強型別和弱型別不是 1 或 0 二元論,而有「程度」的差別。
雖然 PHP 是弱型別,偏向容許隱性型別轉換,但還是有個容忍的限度。
例如企圖將數字和陣列做算術運算,就不再是無傷大雅的 Warning,而會得到 Error:
$x = 123 + array("Apple", "Banana");
echo $x;
執行結果:
PHP Fatal error: Unsupported operand types in /home/cg/root/6938116/main.php on line 3
答案是:NO。
「靜態型別/動態型別」和「弱型別/強型別」代表不同的意義:
舉例來說,Python 是一個動態語言,以下例子企圖對 Python 的數字和字串進行相加:
x = 123 + "Hello Python"
print x + "\n"
會得到以下錯誤:
TypeError: unsupported operand type(s) for +: 'int' and 'str'
因為 Python 雖然是個動態語言,但在型別判斷的嚴格程度上,Python 是一個強型別。
雖然會有些趨勢,例如常看到的靜態語言大部分是強型別。但本質來說,「靜態型別/動態型別」和「弱型別/強型別」沒有絕對關係。
以下是幾種常見程式語言的型別特性:
靜態語言 / 動態語言 | 強型別 / 弱型別 | 程式語言 |
---|---|---|
靜態 | 強 | Java, C# |
靜態 | 弱 | C/C++ |
動態 | 強 | Python, Ruby |
動態 | 弱 | Perl, PHP, JavaScript |
靜態語言又分顯性型別和隱性型別:
下面這張象限圖可以非常清楚看到不同語言各自屬於哪個象限:
(Source: 動態語言與靜態?直譯與編譯?強型別與弱型別?)
這個系列的主題是 JavaScript,本節算是一個對強型別/弱型別的額外探討。
在型別特性的分類上,C 語言被歸類在弱型別,可能比較叫人意外,因為 C 語言的變數無論在宣告或是使用上都非常囉嗦嚴格。
有些文章提到的理由是:因為 C 的 int
可以變成 double
。
這個理由漏洞很大,因為 Java 的 int
也容許被隱性轉成 double
:
double x = 123;
System.out.println(x); // 123.0
那為何 Java 被公認為強型別,C 語言卻被歸類在弱型別?
以下是我目前的簡易理解。
這要來看一下強型別更精確但文謅謅的定義:
強型別(strongly typed):一種語言的所有程式都是 well behaved —— 也就是不可能出現 forbidden behaviors。
C 語言有一個特殊武器 — 指標(pointer),可以對變數記憶體做更細緻的操作,是 C 語言靈活強大的武器,也是各種令人頭痛的記憶體錯位 Bug 溫床。
使用指標有可能發生例如 int
的記憶體 size 卻允許在語法上放入 double
甚至更大的值,導致記憶體溢位,屬於 forbidden behaviors,因而 C 語言被歸類為弱型別:
int value = 12345;
int* ptr = &value;
*ptr = 999999999999999999999999999; // overflow
printf("%d\n", *ptr);
「C 的 int
可以變成 double
」這句話也許沒錯,只是有點語病,想表達的意思應該是:「C 的 int
size 的變數,語法上容許被指派 double
size 值的可能性」。